/* 
 * Copyright (C) 2011 Paul Burke
 * 
 * Licensed under the Apache License, Version 2.0 (the "License"); 
 * you may not use this file except in compliance with the License. 
 * You may obtain a copy of the License at 
 * 
 *      http://www.apache.org/licenses/LICENSE-2.0 
 * 
 * Unless required by applicable law or agreed to in writing, software 
 * distributed under the License is distributed on an "AS IS" BASIS, 
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 
 * See the License for the specific language governing permissions and 
 * limitations under the License. 
 */ 

package com.ipaulpro.afilechooser;

import java.io.File;
import java.io.FileFilter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;

import android.app.ListActivity;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.net.Uri;
import android.os.Bundle;
import android.os.Environment;
import android.text.TextUtils;
import android.util.Log;
import android.view.View;
import android.widget.ListView;

import com.ipaulpro.afilechooser.utils.FileUtils;

/**
 * @author paulburke (ipaulpro)
 */
public class FileChooserActivity extends ListActivity {
	
	private static final boolean mLogging = false; // Set to false to disable logging
	private static final String TAG = "ChooserActivity"; // The log tag

	public static final int REQUEST_CODE = 6384; // onActivityResult request code
	public static final String MIME_TYPE_ALL = "*/*"; // Filter for all MIME types
	
	private static final String PATH = "path";
	private static final String BREADCRUMB = "breadcrumb";
	private static final String HIDDEN_PREFIX = ".";

	private String mPath; // The current file path
	private ArrayList<String> mBreadcrumb = new ArrayList<String>(); // Path history 

	private boolean mExternalStorageAvailable = false;
	private boolean mExternalStorageWriteable = false;
	
	private File mExternalDir;
	private FileListAdapter mAdapter;
	private ArrayList<File> mList = new ArrayList<File>();

	/**
	 * File (not directories) filter.
	 */
	private FileFilter mFileFilter = new FileFilter() {
		public boolean accept(File file) {
			final String fileName = file.getName();
			// Return files only (not directories) and skip hidden files
			return file.isFile() && !fileName.startsWith(HIDDEN_PREFIX);
		}
	};
	
	/**
	 * Folder (directories) filter.
	 */
	private FileFilter mDirFilter = new FileFilter() {
		public boolean accept(File file) {
			final String fileName = file.getName();
			// Return directories only and skip hidden directories
			return file.isDirectory() && !fileName.startsWith(HIDDEN_PREFIX);
		}
	};
	
	/**
	 * File and folder comparator.
	 * TODO Expose sorting option method 
	 */
	private Comparator<File> mComparator = new Comparator<File>() {
		public int compare(File f1, File f2) {
			// Sort alphabetically by lower case, which is much cleaner
			return f1.getName().toLowerCase().compareTo(
					f2.getName().toLowerCase());
		}
	};

	/**
	 * External storage state broadcast receiver. 
	 */
	private BroadcastReceiver mExternalStorageReceiver = new BroadcastReceiver() {
		@Override
		public void onReceive(Context context, Intent intent) {
			if (mLogging) Log.d(TAG, "External storage broadcast recieved: "
					+ intent.getData());
			updateExternalStorageState();
		}
	};
	
	/* *************************** Methods **************************** */

	/**
	 * Activities extending FileChooserActivity must check against this, and implement
	 * the associated Intent Filter in AndroidManifest.xml.
	 * @return True if the Intent Action is android.intent.action.GET_CONTENT.
	 */
	protected boolean isIntentGetContent() {
		final Intent intent = getIntent();
		final String action = intent.getAction();
		if (mLogging) Log.d(TAG, "Intent Action: "+action);
		return Intent.ACTION_GET_CONTENT.equals(action);
	}
	
	/**
	 * Display the Intent Chooser.
	 * @param title Chooser Dialog title.
	 * @param type Explicit MIME data type filter.
	 */
	protected void showFileChooser(String title, String type) {
		if (TextUtils.isEmpty(title)) title = getString(R.string.select_file);		
		if (TextUtils.isEmpty(type)) type = MIME_TYPE_ALL;	
		
		// Implicitly allow the user to select a particular kind of data
		final Intent intent = new Intent(Intent.ACTION_GET_CONTENT); 
		// Specify the MIME data type filter (Must be lower case)
		intent.setType(type.toLowerCase()); 
		// Only return URIs that can be opened with ContentResolver
		intent.addCategory(Intent.CATEGORY_OPENABLE);
		// Display intent chooser
		try {
			startActivityForResult(
					Intent.createChooser(intent, title),REQUEST_CODE);
		} catch (android.content.ActivityNotFoundException e) {
			onFileError(e);
		}
	}
	
	/**
	 * Convenience method to show the File Chooser with the default 
	 * title and have it return all file types.
	 */
	protected void showFileChooser() {
		showFileChooser(null, null);
	}
	
	/**
	 * Fill the list with the current directory contents. 
	 */
	private void fillList() {
		if (mLogging) Log.d(TAG, "Current path: "+this.mPath);
		
		// Set the cuttent path as the Activity title
		setTitle(this.mPath);
		// Clear the list adapter
		this.mAdapter.clear();
		
		// Our current directory File instance
		final File pathDir = new File(mPath);
		
		// List file in this directory with the directory filter
		final File[] dirs = pathDir.listFiles(mDirFilter);
		if (dirs != null) {
			// Sort the folders alphabetically
			Arrays.sort(dirs, mComparator);
			// Add each folder to the File list for the list adapter
			for (File dir : dirs) mList.add(dir);
		}

		// List file in this directory with the file filter
		final File[] files = pathDir.listFiles(mFileFilter);
		if (files != null) {
			// Sort the files alphabetically
			Arrays.sort(files, mComparator);
			// Add each file to the File list for the list adapter
			for (File file : files) mList.add(file);
		}		
		
		if (dirs == null && files == null) {
			if (mLogging) Log.d(TAG, "Directory is empty");
		}
		
		// Assign the File list items as our adapter items
		this.mAdapter.setListItems(mList);
		// Update the ListView
		this.mAdapter.notifyDataSetChanged();
		// Jump to the top of the list
		getListView().setSelection(0);
	}
	
	/**
	 * Keep track of the directory hierarchy.
	 * @param add Add the current path to the directory stack.
	 */
	private void updateBreadcrumb(boolean add) {
		if (add) {			
			// Add the current path to the stack
			this.mBreadcrumb.add(this.mPath);
		} else {
			if (this.mExternalDir.getAbsolutePath().equals(this.mPath)) {
				// If at the base directory, exit the Activity
				onFileSelectCancel();
				finish();
			} else {
				// Otherwise, remove the last path from the stack
				int size = this.mBreadcrumb.size(); 
				if (size > 1) {
					this.mBreadcrumb.remove(size - 1);
					this.mPath = this.mBreadcrumb.get(size - 2);
					
					// Display the new directory contents
					fillList();
				}
			}
		}
	}

	/**
	 * Update the external storage member variables.
	 */
	private void updateExternalStorageState() {
		String state = Environment.getExternalStorageState();
		if (Environment.MEDIA_MOUNTED.equals(state)) {
			this.mExternalStorageAvailable = this.mExternalStorageWriteable = true;
		} else if (Environment.MEDIA_MOUNTED_READ_ONLY.equals(state)) {
			this.mExternalStorageAvailable = true;
			this.mExternalStorageWriteable = false;
		} else {
			this.mExternalStorageAvailable = this.mExternalStorageWriteable = false;
		}
		
		handleExternalStorageState(this.mExternalStorageAvailable,
				this.mExternalStorageWriteable);
	}
	
	/**
	 * Register the external storage BroadcastReceiver.
	 */
	private void startWatchingExternalStorage() {
		IntentFilter filter = new IntentFilter();
		filter.addAction(Intent.ACTION_MEDIA_MOUNTED);
		filter.addAction(Intent.ACTION_MEDIA_REMOVED);
		registerReceiver(this.mExternalStorageReceiver, filter);
		
		updateExternalStorageState();
	}

	/**
	 * Unregister the external storage BroadcastReceiver.
	 */
	private void stopWatchingExternalStorage() {
		unregisterReceiver(this.mExternalStorageReceiver);
	}

	/**
	 * Respond to a change in the external storage state
	 * @param available
	 * @param writeable
	 */
	private void handleExternalStorageState(boolean available, boolean writeable) {
		if (!available && isIntentGetContent()) {
			if (mLogging) Log.d(TAG, "External Storage was disconnected");
			onFileDisconnect();
			finish();
		}
	}
	
	/**
	 * If the activity was interrupted, restore the previous path and breadcrumb
	 * @param savedInstanceState
	 */
	private void restoreMe(Bundle savedInstanceState) {
		if (savedInstanceState != null) {
			if (savedInstanceState.containsKey(PATH)) {
				// Restore the previous path. Defaults to base external storage dir
				this.mPath = savedInstanceState.getString(PATH);//, 
						//Environment.getExternalStorageDirectory().getAbsolutePath());
			}
			if (savedInstanceState.containsKey(PATH)) {
				// Restore the previous breadcrumb
				this.mBreadcrumb = 
					savedInstanceState.getStringArrayList(BREADCRUMB);
			}
		}
	}

	/**
	 * Called when a file is successfully selected by the user.
	 * @param file The file selected.
	 */
	protected void onFileSelect(File file){
		if (mLogging) Log.d(TAG, "File selected: "+file.getAbsolutePath());
	}

	/**
	 * Called when there is an error selecting a file.
	 * @param e The error encountered during file selection.
	 */
	protected void onFileError(Exception e){
		if (mLogging) Log.e(TAG, "Error selecting file", e);
	}

	/**
	 * Called when the user backs out of the file selection process.
	 */
	protected void onFileSelectCancel(){
		if (mLogging) Log.d(TAG, "File selection canceled");
	}

	/**
	 * Called when the external storage (SD) is disconnected.
	 */
	protected void onFileDisconnect(){
		if (mLogging) Log.d(TAG, "External storage disconnected");
	}
	
	/* *************************** Overrides **************************** */

	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);

		// Get the external storage directory.
		this.mExternalDir = Environment.getExternalStorageDirectory();

		// Set the external storage directory as the current path
		this.mPath = this.mExternalDir.getAbsolutePath();	
		// Add the current path to the breadcrumb
		updateBreadcrumb(true);

		this.mAdapter = new FileListAdapter(this);
		// Assign the list adapter to the ListView
		setListAdapter(this.mAdapter);
		
		if (savedInstanceState != null)	{
        		restoreMe(savedInstanceState);
        }
	}

	@Override
	protected void onResume() {
		super.onResume();
		
		if (isIntentGetContent()) {
			startWatchingExternalStorage();
			setContentView(R.layout.explorer);
			fillList();
		}
	}

	@Override
	protected void onPause() {
		super.onPause();
		
		if (isIntentGetContent()) {
			stopWatchingExternalStorage();
		} 
	}
	
	@Override
	public void onBackPressed() {
		updateBreadcrumb(false);
	}
	
	@Override
	protected void onListItemClick(ListView l, View v, int position, long id) {
		super.onListItemClick(l, v, position, id);

		// Get the file that was selected from the file list
		File file = this.mList.get(position);
		// Save the path as our current member variable
		this.mPath = file.getAbsolutePath();
		if (mLogging) Log.d(TAG, "Selected file: "+this.mPath);

		if (file != null) {
			setTitle(this.mPath);
			
			if (file.isDirectory()) {
				// If the selected item is a folder, update UI
				updateBreadcrumb(true);
				fillList();
			} else {
				// Otherwise, return the URI of the selected file
				final Intent data = new Intent();
				data.setData(Uri.fromFile(file));
				setResult(RESULT_OK, data);
				finish();
			}			
		}
	}
	
	@Override
	protected void onActivityResult(int requestCode, int resultCode, Intent data) {
		switch (requestCode) {
		case REQUEST_CODE:	
			if (resultCode == RESULT_OK) {		
				// If the file selection was successful
				try {
					// Get the URI of the selected file
					final Uri uri = data.getData();
					// Create a file instance from the URI
					final File file = new File(FileUtils.getPath(this, uri));
					// Expose the file
					onFileSelect(file);
				} catch (Exception e) {
					onFileError(e);
				}		
			} else if (resultCode == RESULT_CANCELED) {
				onFileSelectCancel();
			}
			break;
		}
		super.onActivityResult(requestCode, resultCode, data);
	}
	
	@Override 
    protected void onSaveInstanceState(Bundle outState) { 
      super.onSaveInstanceState(outState); 
      	// Save the current path and breadcrumb when the activity is interrupted.
      	outState.putString(PATH, mPath);
      	outState.putStringArrayList(BREADCRUMB, mBreadcrumb);
    } 
}